// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: jaewon

#pragma once

#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include <boost/system/error_code.hpp>

#include "coin2/base/cpp_feature.h"
#include "coin2/base/request/http.h"
#include "coin2/base/request/util.h"
#include "coin2/base/time.h"

namespace coin2::base::request {

enum RequestPriorityWeight : int {
  kLow = 0,      // map nghttp2 priority MIN 2^0 = 1
  kMedium = 4,   // map nghttp2 priority Default 2^4 = 16
  kHigh = 6,     // map nghttp2 priority High 2^6 = 64
  kCritical = 8  // map nghttp2 priority MAX 2^8 = 256
};

struct SessionResponse {
  SessionResponse() = default;
  SessionResponse(int64_t ts) : request_ts(ts) {}
  boost::system::error_code error_code;
  const http::request<http::string_body>* raw_request = nullptr;
  const http::response<http::string_body>* raw_response = nullptr;
  int64_t request_ts = 0;
  int64_t response_ts = 0;
};

class Session : public NonCopyable,
                public HttpsConnection::Handler,
                public std::enable_shared_from_this<Session> {
 public:
  using RequestType = HttpsConnection::RequestType;
  using ResponseType = HttpsConnection::ResponseType;
  using CallbackType = std::function<void(boost::system::error_code, const SessionResponse*)>;

  class Callback {
   public:
    virtual ~Callback() = default;
    virtual void OnSessionReady(
        std::weak_ptr<Session> session,
        int64_t rtt,
        const std::string& req_method,
        const std::string& req_target,
        int32_t http_code) = 0;
    virtual void OnSessionError(std::weak_ptr<Session> session) = 0;
  };
  struct RequestContext {
    RequestType request;
    SessionResponse response;
    CallbackType callback;
    boost::system::error_code ec;
    RequestContext(RequestType req, CallbackType cb, int64_t ts)
        : request(req), response(ts), callback(cb) {}
    ~RequestContext() {
      if (0 == response.response_ts) {
        response.response_ts = GetCurrentTimestamp();
      }
      response.raw_request = &request;
      callback(ec, &response);
    }
  };

  Session(
      boost::asio::io_context* io_ctx,
      Callback* callback,
      std::shared_ptr<detail::SimpleObjectPool<RequestContext>> req_ctx_pool,
      ssl::context* ssl_ctx,
      const std::string& host,
      int port,
      const std::string& bind_ip,
      const std::string& http_proxy);
  Session(
      boost::asio::io_context* io_ctx,
      Callback* callback,
      std::shared_ptr<detail::SimpleObjectPool<RequestContext>> req_ctx_pool,
      ssl::context* ssl_ctx,
      const std::string& host,
      int port,
      const std::string& http_proxy);
  ~Session();

  void AsyncHead();
  void AsyncRequest(RequestType req, CallbackType callback);
  void AsyncRequest(std::string_view target, CallbackType callback);
  void AsyncShutdown();  // Fire and forget.

  template <typename Arg, typename CompletionToken>
  auto CoroRequest(Arg&& arg, CompletionToken&& token) {
    using Result = boost::asio::
        async_result<CompletionToken, void(boost::system::error_code, const SessionResponse*)>;
    using Handler = typename Result::completion_handler_type;

    Handler handler(std::forward<CompletionToken>(token));
    Result result(handler);
    AsyncRequest(std::forward<Arg>(arg), handler);
    return result.get();
  }

  bool IsConnected() const { return connection_ && connection_->is_connected(); }
  int64_t Age(int64_t curr_ts) const {
    if (connection_) {
      return connection_->age(curr_ts);
    } else {
      return 0;
    }
  }
  int64_t Age() const { return connection_->age(GetCurrentTimestamp()); }

  void Connect();
  auto Connection() -> std::weak_ptr<HttpsConnection> { return connection_; }
  bool ProcessTimeout(int64_t timeout_start_ts);
  void SetRequestPriorityWeight(RequestPriorityWeight priority_weight) {
    current_request_priority_weight_ = priority_weight;
  }
  auto GetRequestPriorityWeight() const -> RequestPriorityWeight {
    return current_request_priority_weight_;
  }
  auto GetLastRequestTs() const -> int64_t { return last_request_ts_; }
  auto GetLastActiveTs() const -> int64_t { return last_active_ts_; }
  auto GetRequest() const -> const RequestType& { return req_->request; };

 private:
  void SanitizeRequest(RequestType* req);

  void OnConnected(std::weak_ptr<HttpsConnection> conn) final;
  void OnRequestSent(std::weak_ptr<HttpsConnection> conn, const RequestType* req) final;
  void OnResponseReceived(std::weak_ptr<HttpsConnection> conn, const ResponseType* rep) final;
  void OnError(std::weak_ptr<HttpsConnection> conn, boost::system::error_code ec) final;

  boost::asio::io_context* io_ctx_;
  Callback* callback_;
  std::shared_ptr<detail::SimpleObjectPool<RequestContext>> req_ctx_pool_;

  ssl::context* ssl_ctx_;
  const std::string host_;
  const int port_;
  const std::string bind_ip_;

  int64_t last_request_ts_ = 0;
  int64_t last_active_ts_ = 0;

  std::shared_ptr<HttpsConnection> connection_;
  std::shared_ptr<RequestContext> req_;

  const std::string http_proxy_;

  RequestPriorityWeight current_request_priority_weight_ = RequestPriorityWeight::kMedium;
};

}  // namespace coin2::base::request
